package org.jacp.demo.io;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.imageio.ImageIO;


/**
 * extract thumbnails from EXIF in JPEG File
 * 
 * @author Andy Moncsek
 * 
 */
public final class ThumbnailExtractor {

    public final File getThubnail(final File file, final String outputFileName) {
	InputStream in = null;
	try {
	    in = new FileInputStream(file);
	    return getThumbnail(in, outputFileName);
	} catch (final Throwable t) {
	    return null;
	} finally {
	    if (in != null) {
		try {
		    in.close();
		} catch (final Exception e) {
		}
	    }
	}
    }

    public final BufferedImage getThumbnail(File file) {
	InputStream in = null;
	try {
	    in = new FileInputStream(file);
	    return getThumbnail(in);
	} catch (IOException e) {
	    return null;
	} finally {
	    if (in != null) {
		try {
		    in.close();
		} catch (Exception e) {
		    e.printStackTrace();
		}
	    }
	}
    }

    private final BufferedImage getThumbnail(InputStream in) {
	try {
	    byte[] scratch = new byte[128];
	    byte[] thumbnailData = getData(in, scratch);
	    if (thumbnailData == null)
		return null;

	    int headerSize = writeHeader(scratch);
	    final InputStream jpegIn = new CombinedInputStream(
		    new ByteArrayInputStream(scratch, 0, headerSize),
		    new ByteArrayInputStream(thumbnailData, 2,
			    thumbnailData.length - 2));
	    return ImageIO.read(jpegIn);
	} catch (IOException e) {
	    return null;
	}
    }

    /**
     * extract thumbnail form exif data in large jpg file
     * 
     * @param in
     * @param outputFileName
     * @return
     */
    private final File getThumbnail(final InputStream in,
	    final String outputFileName) throws IOException {
	final byte[] scratchArray = new byte[128];
	final byte[] data = getData(in, scratchArray);
	if (data == null) {
	    return null;
	}
	final int headerSize = writeHeader(scratchArray);
	return writeFile(scratchArray, 0, headerSize, data, 2, data.length - 2,
		outputFileName);
    }

    private final byte[] getData(final InputStream input,
	    final byte[] scratchArray) throws IOException {
	final byte[] bytes = scratchArray;
	final MeasuredInputStream inputStream = new MeasuredInputStream(input);
	boolean reverse = false;
	final int offset = 12;
	read(inputStream, bytes, 2, reverse);
	if (!(bytes[0] == -1 && bytes[1] == -40)) {
	    // this didn't have a SOI marker at the top: give up immediately.
	    return null;
	}
	// now we want to find an APP1 marker:
	read(inputStream, bytes, 2, reverse);
	while (bytes[0] == -1 && bytes[1] != -31) {
	    // we found a marker but it's not an APP1 marker.
	    read(inputStream, bytes, 2, reverse);
	    final int dataSize = (bytes[0] & 0xff) * 256 + (bytes[1] & 0xff);
	    inputStream.skip(dataSize);
	    read(inputStream, bytes, 2, reverse);
	}
	if (!(bytes[0] == -1 && bytes[1] == -31)) {
	    // we didn't get our APP1 marker
	    return null;
	}

	read(inputStream, bytes, 2, reverse);
	// this value +2 is where the image data for the main image starts:
	// long length = (b[0] & 0xff)*256+(b[1] & 0xff);
	read(inputStream, bytes, 6, reverse);
	if (!(bytes[0] == 69 && bytes[1] == 120 && bytes[2] == 105
		&& bytes[3] == 102 && bytes[4] == 0 && bytes[5] == 0)) {
	    // this didn't have "Exif" followed by a 0x00, so this isn't an exif
	    // file.
	    return null;
	}

	read(inputStream, bytes, 2, reverse);
	if (bytes[0] == 73 && bytes[1] == 73) { // little endian
	    reverse = true;
	} else if (bytes[0] == 77 && bytes[1] == 77) { // big endian
	    // do nothing
	} else {
	    return null;
	}

	read(inputStream, bytes, 2, reverse);
	if (!(bytes[0] == 0 && bytes[1] == 42)) { // required byte in TIFF
						  // header
	    return null;
	}
	read(inputStream, bytes, 4, reverse); // position of zero-eth IFD

	// the position of the zero-eth IFD
	long pos = (bytes[0] & 0xff) * 256 * 256 * 256 + (bytes[1] & 0xff)
		* 256 * 256 + (bytes[2] & 0xff) * 256 + (bytes[3] & 0xff);

	inputStream.seek(pos + offset);

	final ExifIFD i0 = new ExifIFD(inputStream, reverse);

	read(inputStream, bytes, 4, reverse);

	// the position of the next IFD
	pos = (bytes[0] & 0xff) * 256 * 256 * 256 + (bytes[1] & 0xff) * 256
		* 256 + (bytes[2] & 0xff) * 256 + (bytes[3] & 0xff);

	i0.resolveIFDs(inputStream, reverse, offset);

	ExifIFD i1 = null;
	while (pos != 0 && (i1 == null || i1.getJPEGPointer() < 0)) {
	    inputStream.seek(pos + offset);

	    i1 = new ExifIFD(inputStream, reverse);
	    read(inputStream, bytes, 4, reverse);
	    // the position of the next IFD
	    pos = (bytes[0] & 0xff) * 256 * 256 * 256 + (bytes[1] & 0xff) * 256
		    * 256 + (bytes[2] & 0xff) * 256 + (bytes[3] & 0xff);
	}

	if (i1 != null && i1.getJPEGPointer() > 0) {
	    pos = i1.getJPEGPointer();
	    final int len = (int) i1.getJPEGLength();
	    if (len == 0) {
		return null;
	    }
	    final byte[] thumbnailData = new byte[len];

	    inputStream.seek(offset + pos);
	    read(inputStream, thumbnailData, len, false);
	    if (!(thumbnailData[0] == -1 && thumbnailData[1] == -40)) {
		// didn't start with a SOI marker: something's wrong
		return null;
	    }
	    final byte[] d = new byte[4];
	    System.arraycopy(thumbnailData, len - d.length, d, 0, d.length);
	    return thumbnailData;
	}
	return null;
    }

    private final void read(final InputStream in, final byte[] dest,
	    final int amt, final boolean reverse) throws IOException {
	int ctr = 0;
	int t = in.read(dest, ctr, amt - ctr);
	while (t + ctr != amt && t != -1) {
	    ctr += t;
	    t = in.read(dest, ctr, amt - ctr);
	}
	if (t != -1) {
	    ctr += t;
	}

	if (ctr != amt) {
	    throw new EOFException();
	}

	if (reverse) {
	    t = amt / 2;
	    for (int i = 0; i < t; i++) {
		final byte k = dest[amt - 1 - i];
		dest[amt - 1 - i] = dest[i];
		dest[i] = k;
	    }
	}
    }

    /**
     * Writes 42 bytes of header info for an exif JPEG file.
     * 
     * @param array
     *            an array of at least 42 bytes to write to.
     * @return the number of bytes written. This will be 42.
     */
    private int writeHeader(final byte[] array) {
	array[0] = -1; // Exif header
	array[1] = -40;
	array[2] = -1; // APP1 marker
	array[3] = -31;
	array[4] = 0; // length of APP1 marker
	array[5] = 38;
	array[6] = 69; // Exif marker
	array[7] = 120;
	array[8] = 105;
	array[9] = 102;
	array[10] = 0;
	array[11] = 0;
	array[12] = 73; // TIFF Header
	array[13] = 73;
	array[14] = 42;
	array[15] = 0;
	array[16] = 8;
	array[17] = 0;
	array[18] = 0;
	array[19] = 0;
	// 0th IFD
	array[20] = 1; // header: only write 1 value
	array[21] = 0;
	array[22] = 0; // FileSource, UNDEFINED [0 0 0 1] [0 0 0 3]
	array[23] = -93;
	array[24] = 7;
	array[25] = 0;
	array[26] = 1;
	array[27] = 0;
	array[28] = 0;
	array[29] = 0;
	array[30] = 3;
	array[31] = 0;
	array[32] = 0;
	array[33] = 0;

	array[34] = 0;
	array[35] = 0;
	array[36] = 0;
	array[37] = 0;
	array[38] = 0;
	array[39] = 0;
	array[40] = 0;
	array[41] = 0;

	return 42;
    }

    private final File writeFile(final byte[] array1, final int array1offset,
	    final int array1size, final byte[] array2, final int array2offset,
	    final int array2size, final String outputFileName)
	    throws IOException {
	FileOutputStream out = null;
	try {
	    final File file = new File(outputFileName);
	    out = new FileOutputStream(file);
	    out.write(array1, array1offset, array1size);
	    out.write(array2, array2offset, array2size);
	    return file;
	} finally {
	    if (out != null) {
		try {
		    out.close();
		} catch (final IOException e) {
		    e.printStackTrace();
		}
	    }
	}
    }

}
